这个项目是我们与浦厂智能部合作的一个项目。

== 网络架构概述 ==
[[File:PIS200KM系统拓扑结构图.png]]

如上图所示，车厢之间以及车厢内的黑色实线表示网线。车厢之间的绿色虚线表示568线，作为冗入的音频传输线，在车厢网络不能工作时起工作。车厢内的网络设备节点之间的蓝色虚线表示485线，用来接收网络功放的车厢号拨码开关的值。车厢内的DU与屏连接的蓝色实线是控制屏设备的485线。

== MVB通信 ==

MVB通信由一块专门的MVB板卡（Duagon）提供服务。

=== MVB开发调试板 ===

工作目录 '''/home/ubuntu/MVB_working-NanJing/D013_modded/D013 Linux Driver_Serial/linux_d013_ser'''
  sudo make
  sudo ./tcn_demo

主程序：'''/home/ubuntu/MVB_working-NanJing/D013_modded/D013 Linux Driver_Serial/based_on/d-000543-nnnnnn/sources/src/tcn_demo.c'''

=== 司机室车厢 ===

一列车有前后司机室车厢，任何时候其中的一个司机室车厢是处于激活状态，作为主司机室，另外一个作为从司机室。 通过激活钥匙可以切换主从司机室。司机室车厢中有一个多媒体主机MSU和一个广播主机PSU，这两个主机直接通过网线直连。

=== 乘客车厢 ===

一列车中的每一节乘客车厢中有一个客室主机PCU。

客室主机PCU的面板由电源模块PW、交换机SW、AMP、DU、HDD、CCTV组成。其中DU通过485控制动态地图和车内屏的显示。其中的AMP面板有一个车厢号码的拔码开关，用来设置当前所在车厢的车厢号，当前车厢的所有网络设备通过485线可以检测到AMP设备的拔码开关的车厢号。

客室主机PCU中连接的网络节点设备类型有DU、报警面板PECU、CCTV、客室摄像头、AMP、IP电视。

=== 设备信息 ===

{| class="wikitable sortable"
|-
! 设备类型 !! 设备类型编号 !! 操作系统 !! 远程登录用户名 || 密码 || 主要服务组成
|-
| PIS主机 || 11 || Ubuntu14.04 64位|| ntdeck  || ntdeck || luna-pudge-broadcast、luna-pudge-ipalloc、ntpis
|-
| 监控触摸屏 || 无 || Ubuntu14.04 64位 || ntdeck  || ntdeck || ntpis-cmon
|-
| 网络功放 || 31  || ARM Linux    || root  || 123456 || luna-pudge-2digit、luna-pudge-console
|-
| PECU || 110开始  || ARM Linux  || root  || 123456 || luna-pudge-ipalloc、luna-pudge-console
|-
| DACU || 81 ||ARM Linux || root  || 123456 || luna-pudge-ipalloc、luna-pudge-server、ntdriver-box
|-
| DU || 21 || ARM Ubuntu 14.04 || root  || ntdeck || pudge-led-hb、luna-pudge-ipalloc、udp_serialport.rb
|-
| VES || 50 || Ubuntu 14.04 64位 || ntdeck  || ntdeck || luna-pudge-ipalloc、luna-vss、luna-msync
|}

== 优先级控制 ==

{| class="wikitable sortable"
|-
| 操作|| PA || PEI || CI || PEB || DVA || 优先级
|-
| PA || —  ||  ×  ||  × ||  ×  ||  × ||  高
|-
| PEI||   ||  —  ||  × ||  ×   ||  × ||
|-
| CI ||   ||     ||  —  ||  √    || √  ||
|-
| PEB ||   ||     ||    ||   —   || ×  ||
|-
| DVA  ||   ||     ||    ||      || —  ||
|-
| 高 ||   ||     ||    ||      ||   || 低
|}

5大功能优先级从高到低：
PA：人工广播；PEI：乘客对讲；CI：司机对讲；PEB：预录紧急广播；DVA：数字报站。

表中打钩表示兼容，功能可同时进行；
打叉表示不兼容，当有高优先级功能正在进行，低优先级功能不能激活；当有低优先级功能正在进行，高优先级功能可以激活且将其打断。

例如：
1.当正在进行人工广播，此时点击触摸屏欲进行预录紧急广播，应无响应，忽略此请求。
2.当正在进行数字报站，此时想要进行人工广播，应打断原报站语音功能，激活新功能。


== 功能接口和协议 ==
=== 心跳广播协议 ===

心跳广播协议（LunaHeartBeat）用与在当前的PIS网络中广播自身在线的信息，通过UDP广播到全网中，广播地址是'''255.255.255.255''',端口是4096， 数据以大端的方式排列，数据包格式如下：

{| class="wikitable sortable"
|-
! 帧头 4 !! 设备类型 2 !! COOKIE 4 !! 消息长度 2 !! 消息体 !!  CRC 2
|-
| 0x4C 0x55 0x48 0x42，即"LUHB"4个字符 || 设备类型，见LunaHeartBeat项目 || 32位随机无符号整数 || 消息体的长度，16位随机无符号整数 || 消息体内容 || 从第0位到CRC之前的数据进行CRC验证
|}

一般建议每2秒发送一次数据包。

当前的设备类型在 luna-heartbeat的头文件lhb.h中有定义，如下：

<pre>
typedef enum
{
    LHB_SERVICE_TYPE_NONE  = 0,
    LHB_SERVICE_TYPE_PUDGE = 1,
    LHB_SERVICE_TYPE_PANEL = 2,
    LHB_SERVICE_TYPE_AMP   = 3,
    LHB_SERVICE_TYPE_PIS   = 5,
    LHB_SERVICE_TYPE_DACU  = 6,
    LHB_SERVICE_TYPE_BRCU  = 7,
    LHB_SERVICE_TYPE_CMON  = 8,
    LHB_SERVICE_TYPE_DU    = 9,
    LHB_SERVICE_TYPE_VSS   = 10,
    LHB_SERVICE_TYPE_LMC   = 11,

    LHB_SERVICE_TYPE_CCTV_HOST = 50,
    LHB_SERVICE_TYPE_CCTV_TERM = 51
}LHBServiceType;
</pre>

以上的广播协议已经有封装好了库SO，头文件是<lhb/lhb.h>, 项目名称叫做luna-heartbeat，分为发送端和接收端，通过其中的示例代码src/test.c可以了解其用法。

接收端的初始化：

    lhb_service_init(NULL, iface, 0, LHB_SERVICE_TYPE_PIS, 1);
    lhb_service_payload_feed(lhb_test_service_payload_data_feed_cb, NULL);

lhb_service_init函数的第四个参数用与说明当前的设备类型。lhb_service_payload_feed函数用于设置心跳包的额外数据。

<pre>
static void lhb_test_service_payload_data_feed_cb(guint8 *data, guint16 *size,
    gpointer user_data)
{
    memcpy(data, "Hello, world!", 14);
    *size = 14;
}
</pre>

=== PIS报站协议 ===


PIS报站协议是PIS主机通过HTTP的方式提供服务的，包括WEB UI的HTML接口，报站接口、预录广播接口和背景音乐的接口。首先需要获取当前车厢网络中PIS主机的IP地址，这个可以通过心跳广播协议获取得到，相关的解析代码片断如下：

1. 初始化lhb接收端，并设置数据接收的回调函数：

  lhb_client_init(NULL, "eth0", 0);
  lhb_client_set_receive_callback(client_heartbeat_receive_cb, this);

2. 接收心跳数据的回调函数：

  static void client_heartbeat_receive_cb(struct sockaddr_in *source_addr,
      LHBServiceType service_type,
      const guint8 *payload_data, guint16 payload_size, gpointer user_data)
  {
      PudgeClient *pudge_client = static_cast<PudgeClient *>(user_data);
      gchar addrstr[INET_ADDRSTRLEN+1] = {0};
      inet_ntop(AF_INET, &(source_addr->sin_addr), addrstr, INET_ADDRSTRLEN);
      pudge_client->process_heartbeat_signal(QString(addrstr), service_type, payload_data, payload_size);
  }

3. 解析心跳数据的函数，在这个函数中，只需要解析设备类型是PIS主机的数据包，PIS主机发送的数据包里发送了额外的数据，是一个JSON格式，描述了当前路线ID、路线名称、当前站点ID、当前站点状态、预路音频ID、背景音乐ID，是否是主服务器。

<pre>
{ "route_id" : 1, "route_name" : "G888", "station_id" : 2, "station_status" : 2, "voice_id" : -1, "bg_voice_id" : -1, "is_master" : 1 }
</pre>

<pre>
void PudgeClient::process_heartbeat_signal(const QString &host, int host_type, const quint8 *payload_data, quint16 payload_size) {
    switch(host_type) {
        case LHB_SERVICE_TYPE_PUDGE: {
            break;
        }
        case LHB_SERVICE_TYPE_PIS: {
            bool is_master = false; // reference used in follow process_heartbeat_data() fuction.
            iPisClient.process_heartbeat_data(payload_data, payload_size, is_master);

            if(!is_master)
                return;
            bool pis_host_online_status_changed = false;
            if(!_pis_host_online_) {
                LedController::instance().light_connected_led(true);
                pis_host_online_status_changed = true;
                _pis_host_online_ = true;
            }
            if(_pis_host_ != host || pis_host_online_status_changed) {
                _pis_host_ = host;
                PisLogger::instance().info(QString("Found pis host %1").arg(host));
                emit pis_host_changed(_pis_host_);
                LedController::instance().light_connected_led(true);

            } else {
                _pis_host_last_onilne_ = QTime::currentTime();
            }
            break;
        }
        case LHB_SERVICE_TYPE_VSS: {
            break;
        }
        case LHB_SERVICE_TYPE_LMC: {
            break;
        }
        default: {
            break;
        }
    }
}
</pre>

==== PIS报站协议的测试方法 ====

为了快速验证PIS报站协议的接口，可以使用curl这个命令行工具进行调试，curl的安装方法:

    sudo apt-get install curl

curl发送GET请求： curl https://example.com/resource.cgi

curl发送POST请求： curl curl --data "" https://example.com/resource.cgi

curl发送POST请求并携带数据： curl --data "param1=value1&param2=value2" https://example.com/resource.cgi

==== WEB UI的HTML接口 ====

WEB UI的HTML接口可以直接用网页浏览器直接访问来测试。

* 报站界面 http://PIS_SERVER_IP:3000/
* 预录音频界面 http://PIS_SERVER_IP:3000/voices.html
* 背景音乐界面 http://PIS_SERVER_IP:3000/bg_music.html

==== 获取所有路线 ====

GET http://192.168.104.11:3000/routes.json
返回结果：

    {
      "routes": [
        {
          "id": 1,
          "name": "T65",
          "current_route_station_id": 0,
          "current_station_status": 1,
          "running": false,
          "position": 0,
          "direction": 0,
          "stations_count": 4,
          "reverse_route_id": 2,
          "reverse_route_name": ""
        },
        {
          "id": 2,
          "name": "T66",
          "current_route_station_id": 6,
          "current_station_status": 1,
          "running": false,
          "position": 0,
          "direction": 1,
          "stations_count": 6,
          "reverse_route_id": 1,
          "reverse_route_name": ""
        },
        {
          "id": 3,
          "name": "G888",
          "current_route_station_id": 14,
          "current_station_status": 2,
          "running": true,
          "position": 0,
          "direction": 0,
          "stations_count": 26,
          "reverse_route_id": 4,
          "reverse_route_name": ""
        },
        {
          "id": 4,
          "name": "G887",
          "current_route_station_id": 37,
          "current_station_status": 1,
          "running": false,
          "position": 0,
          "direction": 1,
          "stations_count": 26,
          "reverse_route_id": 3,
          "reverse_route_name": ""
        }
      ],
      "count": 4,
      "current_route_id": 3
    }

==== 获取路线站点数据 ====

GET http://PIS_SERVER_IP:3000/routes/1.json

    {
      "route": {
        "id": 1,
        "name": "T65",
        "current_route_station_id": 0,
        "current_station_status": 1,
        "running": false,
        "position": 0,
        "direction": 0,
        "stations_count": 4
      },
      "stations": [
        {
          "id": 1,
          "station_id": 1,
          "name": "北京站",
          "position": 0
        },
        {
          "id": 2,
          "station_id": 2,
          "name": "徐州站",
          "position": 1
        },
        {
          "id": 3,
          "station_id": 3,
          "name": "蚌埠站",
          "position": 2
        },
        {
          "id": 4,
          "station_id": 4,
          "name": "南京站",
          "position": 3
        }
      ]
    }

==== 获取当前路线站点状态 ====

GET http://192.168.104.11:3000/station_status.json
返回结果：
    
    {
      "current_station_id": 14,
      "current_station_status": 2,
      "current_station_index": 3
    }
        
测试方法： curl http://192.168.104.11:3000/station_status.json
        
==== 站点播报 ====
    
POST http://PIS_SERVER_IP:3000/pa?route_station_id=14&status=2

返回结果：
ok

==== 获取预录音频 ====

GET http://PIS_SERVER_IP:3000/voices.json
返回结果：

    {
      "voices": [    
        {
          "id": 11,
          "remark": "请给需要帮助的乘客让个座",
          "ticker": "请给需要帮助的乘客让个座",
          "file_name": "voices/请给需要帮助的乘客让个座.mp3",
          "position": 0,
          "play_duration": 4
        },
        {
          "id": 12,
          "remark": "禁烟提示",
          "ticker": "女士们、先生们，本次列车是无烟列车，请不要在车内吸烟，感谢您的配合",
          "file_name": "voices/禁烟提示.mp3",
          "position": 0,
          "play_duration": 8
        },
        {
          "id": 13,
          "remark": "临时停车",
          "ticker": "女士们、先生们，列车没有到站，现在是临时停车，请您在座位上耐心等候，不要随意走动，感谢您的配合",
          "file_name": "voices/临时停车.mp3",
          "position": 0,
          "play_duration": 12
        }    
      ],
      "count": 2,
      "current_voice_id": -1
    }

==== 获取音量接口 ====

GET http://PIS_SERVER_IP:3000/volumes.json
返回结果：

  {
    "station_pa": 20,
    "broadcast": 20,
    "bgmusic": 4
  }

==== 获取广播状态 ====

GET http://PIS_SERVER_IP:3000/voice_broadcast_status.json

返回结果：
    
    {
      "voice_id": -1,
      "bg_voice_id": -1
    }

==== 播放预录音频播放 ====

GET http://PIS_SERVER_IP:3000/play_voice?id=音频记录ID

返回结果：
ok

==== 播放背景音乐 ====

GET http://PIS_SERVER_IP:3000/play_bg_voice?id=音频记录ID

返回结果：
ok

    
==== 停止预录音频播放 ====

GET http://PIS_SERVER_IP:3000/stop_voice

返回结果：
ok

==== 获取背景音月列表 ====

GET http://PIS_SERVER_IP:3000/bg_voices.json
返回结果：

{
  "voices": [
    {
      "id": 1,
      "remark": "tianzhiheng",
      "ticker": "",
      "file_name": "bg_voices/天之痕.mp3",
      "position": 0,
      "play_duration": 228
    },
    {
      "id": 2,
      "remark": "梦中的婚礼",
      "ticker": "",
      "file_name": "bg_voices/梦中的婚礼.mp3",
      "position": 0,
      "play_duration": 232
    },
    {
      "id": 3,
      "remark": "天空之城",
      "ticker": "",
      "file_name": "bg_voices/天空之城.mp3",
      "position": 0,
      "play_duration": 253
    },
    {
      "id": 4,
      "remark": "322_Fly away",
      "ticker": "",
      "file_name": "bg_voices/322_Fly away.mp3",
      "position": 0,
      "play_duration": 251
    },
    {
      "id": 5,
      "remark": "龙猫",
      "ticker": "",
      "file_name": "bg_voices/龙猫.mp3",
      "position": 0,
      "play_duration": 257
    },
    {
      "id": 6,
      "remark": "Angel",
      "ticker": "",
      "file_name": "bg_voices/Angel.mp3",
      "position": 0,
      "play_duration": 271
    },
    {
      "id": 7,
      "remark": "光辉岁月",
      "ticker": "",
      "file_name": "bg_voices/光辉岁月.mp3",
      "position": 0,
      "play_duration": 298
    },
    {
      "id": 8,
      "remark": "童年的回忆",
      "ticker": "",
      "file_name": "bg_voices/童年的回忆.mp3",
      "position": 0,
      "play_duration": 238
    }
  ],
  "count": 8,
  "current_voice_id": -1
}

==== 停止背景音乐播放 ====

GET http://PIS_SERVER_IP:3000/stop_bg_voice

返回结果：
ok



==== PIS数据库结构 ====

/var/lib/ntpis/data.db

===== routes表 =====

id, name, current_route_station_id, current_station_status, current_station_name, stations_count, direction, reverse_route_id, reverse_route_name, running, position

* current_station_status：0预到站，1到站，2出站
* direction： 0上行，1下行

===== route_stations表 =====

id, station_id, station_name, position, route_id, position, ticker_in, ticker_at, ticker_out

===== 报站音频 =====

/var/lib/ntpis/station_voices/{route_station_id}_{in,at,out}.mp3

=== 司机对讲 ===

司机对讲服务对应的程序是LunaPudgeServer，端口是2101，TCP连接。数据都是JSON格式。

==== 发起对讲 ====

    QString command_json = QString("{\"command\": \"unicast-outgoing-call\", \"type\": \"driver\", \"dialno\": \"%1\", \"priority\": 0  }\n").arg(target_dialno);
    QByteArray data;
    data.append(command_json);
    socket.write(data);

==== 接听对讲来电 ====

    QString command = QString("{\"command\": \"unicast-incoming-accept\", \"uuid\": \"%1\" }\n").arg(call_id);
    QByteArray data;
    data.append(command);
    socket.write(data);

==== 拒绝接听来电 ====

    QString command = QString("{\"command\": \"unicast-stop\", \"uuid\": \"%1\", \"reason\": \"user\" }\n").arg(call_id);
    QByteArray data;
    data.append(command);

    int result = socket.write(data);

==== 停止对讲 ====

    QString command_json = QString("{\"command\": \"unicast-stop\", \"uuid\": \"%1\" }\n").arg(_current_call_->uuid());
    QByteArray data;
    data.append(command_json);
    socket.write(data);

==== 发起人工广播 ====

    QByteArray data("{\"command\": \"broadcast-microphone-start\", \"channel\": 0 }\n");
    socket.write(data);

==== 停止人工广播 ====

    QByteArray data("{\"command\": \"broadcast-microphone-stop\" }\n");
    socket.write(data);

=== 乘客紧急呼叫 ===
=== DU协议 ===

DU是控制信息屏和动态地图的控制单元，它与屏是通过485连接的。

{| class="wikitable sortable"
|-
! 屏类型 !! 发送的内容 !! 说明
|-
| 车外屏显示 || ntroute*15:40*G887*松江南站*杨高中路 || 发送当前时间、起点站、终点站到车外屏
|-
| 动态地图 || ntroute2*AA1A0EBB020214000102FF || 详情见LED动态地图的协议
|-
| 车内信息显示 || 0*女士们、先生们，本次列车是无烟列车，请不要在车内吸烟，感谢您的配合 || 发送字幕到车内显示屏
|}